/*
 * RendererSwing
 * Version: 0.1
 * Date: 2008-10-25
 * Author: gp08-nhn
 */

package keyboardhero;

import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.awt.image.*;

import javax.swing.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import keyboardhero.Util.*;
import keyboardhero.MidiSequencer.*;
import keyboardhero.Connection.Client;
import keyboardhero.Game.*;
import keyboardhero.Graphs.ScoreMessage;

/**
 * Class for graphical display using a {@link javax.swing.JPanel} as a canvas and methods of the
 * swing library, implementing the abstract interface {@link keyboardhero#GraphRenderer}.
 * 
 * @author gp08-nhn
 * 
 */
final class RendererSwing extends JPanel implements GraphRenderer {
	
	//-------------------- VARIABLES --------------------
	
	// ××××× CORE VARIABLES ×××××
	
	/**
	 * A Canvas is a serializable object, thus it requires a unique Serial Version ID.
	 * 
	 * @see java.io.Serializable
	 */
	private static final long serialVersionUID = -9197029616735010067L;
	
	// ××××× COMMON VARIABLES ×××××
	
	private double ratio = 1;
	
	// ××××× BACKGROUND VARIABLES ×××××
	
	// Obj<Image> is used instead of Images, because then the same method can be used to change them -> Util.resizeImage (refreshDependSize just passes the values)
	// it's bit like reference passing (& sign in C)
	private Image bgAllOrig, bgTopOrig, bgMainOrig;
	private Obj<Image> bgTop = new Obj<Image>(null, new Runnable() {
		public void run() {
			repaint();
		}
	});
	private Obj<Image> bgMain = new Obj<Image>(null, new Runnable() {
		public void run() {
			repaint();
		}
	});
	
	// ××××× TOP VARIABLES ×××××
	
	/** Caching field to store the appropriate font used in top part of the displaying screen. */
	private Font topFontLabel, topFontValue, topFontValueItalic, topFontButton, topFontPlayers, topFontPlayersItalic,
			topFontScores;
	/** Caching field to store the appropriate dimension of top part of the displaying screen. */
	private int topHeight;
	/** A ProgressBar showing the current position in the song playing. */
	private JProgressBar progressBar;
	private JButton pause, fullscreen, randomgame;
	private int row1, row2, row3, row4, row5, row6;
	private int col1, col2, col3, col4, col5, col6;
	private int col1w, col3w, col34w;
	
	// ××××× PAUSED VARIABLES ×××××
	
	private Font pausedFont;
	private int pausedDiff;
	
	// ××××× MIDDLE VARIABLES ×××××
	
	/** Catching field setting the font of the middle part of the displaying screen. */
	private Font mdFont;
	/** Catching field to store the dimensions of the middle part. */
	private int mdHeight, mdTop, mdBottom;
	private int mdLetters;
	private int mdBottomU;
	private int mdArc;
	private int msgImageY;
	
	// ××××× KEYBOARD VARIABLES ×××××
	
	private boolean kbActive;
	private int kbCount;
	/** Catching field setting the font for the letters on the keys of the keyboard */
	private Font kbFont;
	/** Catching field for dimensions of the keyboard ( */
	private int kbStart, kbWWidth, kbBWidth, kbMin, kbMax;
	private int kbWWidthU, kbLeftU;
	private int kbTop, kbBottom, kbWHeight, kbBHeight, kbWidth, kbLeft, kbRight, kbWLBottom, kbBLBottom;
	private int kbLetters;
	
	//-------------------- CORE FUNCTIONS --------------------
	/**
	 * Constructor calling its ancestor and adding components to the GUI such as progress bar, pause
	 * button and full screen button. Further, it also defines the actions to be performed by the
	 * "Pause" and "Full screen" button when pressed.
	 */
	RendererSwing() {
		super();
		
		setBackground(Graphs.getColor("sidePanelBg"));
		
		addComponentListener(new ComponentAdapter() {
			public void componentResized(final ComponentEvent e) {
				resized(e.getComponent().getWidth());
			}
		});
		
		setLayout(null);
		
		add(progressBar = new JProgressBar());
		
		add(pause = new JButton());
		Util.updateButtonText(pause, "Pause");
		pause.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				Game.togglePause();
			}
		});
		pause.setEnabled(false);
		
		add(fullscreen = new JButton());
		Util.updateButtonText(fullscreen, "FullScreen");
		fullscreen.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				KeyboardHero.toogleFullScreenMode();
			}
		});
		
		add(randomgame = new JButton());
		Util.updateButtonText(randomgame, "NewRandomGame");
		randomgame.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				Game.newRandomGame();
			}
		});
	}
	
	/**
	 * Method for drawing on the canvas the components. It calls its ancestor. Then it sets the
	 * rendering algorithm with text antialiasing hint key (not for other shapes) for the text
	 * antialiasing hint value. Finally, it calls the method {@link Graphs#draw(Graphics)} of the
	 * class Graphs to draw the component into the graphics context g.
	 * 
	 * @param g
	 *            the Graphics object to protect.
	 * @see javax.swing.JPanel#paintComponent(Graphics)
	 */
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		((Graphics2D)g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
		
		Graphs.draw(g);
	} // end method
	
	//-------------------- COMMON FUNCTIONS --------------------
	/**
	 * Method for scaling a value
	 * 
	 * @param v
	 *            double value to be scaled.
	 * @return integer value equal to v scaled with ratio.
	 * @see #ratio
	 */
	private final int scale(double v) {
		return (int)(v * ratio);
	}
	
	/**
	 * Creates a new font scaled by the ratio.
	 * 
	 * @param fontName
	 *            the name of the font family.
	 * @param fontStyle
	 *            the style of the font.
	 * @param fontSize
	 *            the relative size of the font.
	 * @return the new font object created.
	 * @see java.awt.Font
	 */
	private final Font getNewFont(String fontName, int fontStyle, int fontSize) {
		return new Font(fontName, fontStyle, scale(fontSize));
	}
	
	/**
	 * Draws a String to the centre of the area specified by the parameter {@code width}.
	 * 
	 * @param g
	 *            the Graphic context to which the string should be drawn.
	 * @param frc
	 *            the font render context to correctly measure the text.
	 * @param font
	 *            the font to be used for the drawing.
	 * @param str
	 *            the value of the text to be drawn.
	 * @param x
	 *            the x coordinate where to draw the string.
	 * @param y
	 *            the y coordinate where to draw the string.
	 * @param width
	 *            the width of the area to which the string should be drawn.
	 */
	private final void drawStringCenter(Graphics g, FontRenderContext frc, Font font, String str, int x, int y, int width) {
		g.drawString(str, x + (width - (int)font.getStringBounds(str, frc).getWidth()) / 2, y);
	}
	
	/**
	 * Draws a String to the right of the area specified by the parameter {@code width}.
	 * 
	 * @param g
	 *            the Graphic context to which the string should be drawn.
	 * @param frc
	 *            the font render context to correctly measure the text.
	 * @param font
	 *            the font to be used for the drawing.
	 * @param str
	 *            the value of the text to be drawn.
	 * @param x
	 *            the x coordinate where to draw the string.
	 * @param y
	 *            the y coordinate where to draw the string.
	 * @param width
	 *            the width of the area to which the string should be drawn.
	 */
	private final void drawStringRight(Graphics g, FontRenderContext frc, Font font, String str, int x, int y, int width) {
		g.drawString(str, x + width - (int)font.getStringBounds(str, frc).getWidth(), y);
	}
	
	/**
	 * Method for drawing on the screen when the Pause button was pressed. It displaying the String
	 * "Paused" with the corresponding font and colour.
	 * 
	 * @param g
	 *            the Graphic context to which the "Paused" text should be drawn.
	 */
	public void drawPausedScreen(Graphics g) {
		g.setFont(pausedFont);
		final String str = Util.getMsg("Paused");
		Rectangle2D bounds = pausedFont.getStringBounds(str, ((Graphics2D)g).getFontRenderContext());
		final int x = ((getWidth() - (int)bounds.getWidth()) / 2);
		final int y = mdTop + ((mdHeight - (int)bounds.getHeight()) / 2);
		g.setColor(Graphs.getColor("frontShadow"));
		g.drawString(str, x + pausedDiff, y + pausedDiff);
		g.setColor(Graphs.getColor("front"));
		g.drawString(str, x, y);
	}
	
	//-------------------- EVENT FUNCTIONS --------------------
	/**
	 * Event method calling the appropriate refreshing methods.
	 * 
	 * @see #refreshDependKeys
	 * @see #refreshDependKeysAndSize
	 * @see #refreshDependLettersAndKeysAndSize
	 */
	public void keysChanged() {
		refreshDependKeys();
		refreshDependKeysAndSize();
		refreshDependLettersAndKeysAndSize();
	}
	
	/**
	 * Event method called when the displaying area resized. It recalculates the ratio according the
	 * to parameter {@code newWidth}, and calls the appropriate refreshing methods. Finally, it
	 * repaints the screen.
	 * 
	 * @param newWidth
	 *            the new width of the displaying area.
	 * @see #refreshDependSize
	 * @see #refreshDependKeysAndSize
	 * @see #refreshDependLettersAndKeysAndSize
	 */
	void resized(double newWidth) {
		ratio = newWidth / Graphs.WIDTH;
		if(ratio <= 0) {
			ratio = 1;
		}
		
		refreshDependSize();
		refreshDependKeysAndSize();
		refreshDependLettersAndKeysAndSize();
		repaint();
		
		if(Util.RUNTIME.freeMemory() < 10485760 || (Util.RUNTIME.maxMemory() - Util.RUNTIME.totalMemory()) < 52428800) {
			Util.RUNTIME.gc();
		}
	}
	
	public void keyboardStateChanged() {
		refreshDependKeyboardStateAndSize();
	}
	
	public void lettersChanged() {
		refreshDependLetters();
		refreshDependLettersAndKeysAndSize();
	}
	
	/**
	 * Method called to actualise the dimension of the keyboard using the first and the last key
	 * pressed by the user, given by the methods {@link Graphs#getFirstKeyInMidi()} and
	 * {@link Graphs#getLastKeyInMidi()}. It also update the field {@code kbWWidthU}, the width of
	 * the white keys, according to the {@link Graphs#WIDTH} and the number of keys {@code kbCount}.
	 * 
	 */
	private void refreshDependKeys() {
		// KEYBOARD
		kbMin = Key.toKey(Graphs.getFirstKeyInMidi());
		kbMax = Key.toKey(Graphs.getLastKeyInMidi());
		kbCount = kbMax - kbMin + 1;
		kbStart = kbCount * 7 + kbMin;
		
		kbWWidthU = (Graphs.WIDTH - 1) / kbCount;
		kbLeftU = (Graphs.WIDTH - (kbCount * kbWWidthU)) / 2;
	}
	
	/**
	 * Method called to update the fonts for the paused mode -field {@code pausedFont} and for the
	 * top part components -labels, buttons, value areas. It also update and scale the fields for
	 * the dimension of the top part {@code topHeight} {@code topWidth}, of the keyboard
	 * {@code kbBottom} {@code kbBHeight} {@code kbWLBottom} {@code kbBLBottom} and of the middle
	 * top. It moves and resizes the progress bar {@code progressBar}
	 * 
	 * 
	 */
	private void refreshDependSize() {
		// BACKGROUND
		final int width;
		if((width = getWidth()) != 0) {
			if(bgTopOrig != null) Util.resizeImage(bgTopOrig, width, bgTop, Graphs.SCALE_HINT);
			if(bgMainOrig != null) Util.resizeImage(bgMainOrig, width, bgMain, Graphs.SCALE_HINT);
		}
		
		// TOP
		topHeight = scale(getMiddleTop());
		topFontLabel = getNewFont(Font.SANS_SERIF, Font.ITALIC | Font.BOLD, 11);
		topFontValue = getNewFont(Font.SANS_SERIF, Font.BOLD, 11);
		topFontValueItalic = getNewFont(Font.SANS_SERIF, Font.BOLD | Font.ITALIC, 11);
		topFontButton = getNewFont(Font.SANS_SERIF, Font.PLAIN, 11);
		topFontPlayers = getNewFont(Font.SANS_SERIF, Font.PLAIN, 10);
		topFontPlayersItalic = getNewFont(Font.SANS_SERIF, Font.ITALIC, 10);
		topFontScores = getNewFont(Font.SANS_SERIF, Font.PLAIN, 10);
		
		row1 = scale(12);
		row2 = scale(27);
		row3 = scale(42);
		row4 = scale(57);
		row5 = scale(70);
		row6 = scale(90);
		col1 = scale(15);
		col1w = scale(90);
		col2 = scale(110);
		col3 = scale(220);
		col3w = scale(95);
		col4 = scale(325);
		col34w = scale(200);
		col5 = scale(410);
		col6 = scale(485);
		
		final int buttonw = scale(100);
		final int buttonh = scale(18);
		if(pause != null) {
			pause.setBounds(col1, row5 - buttonh / 2, buttonw, buttonh);
			pause.setFont(topFontButton);
		}
		if(fullscreen != null) {
			fullscreen.setBounds(scale(125), row5 - buttonh / 2, buttonw, buttonh);
			fullscreen.setFont(topFontButton);
		}
		if(randomgame != null) {
			randomgame.setBounds(col1, row6 - buttonh / 2, scale(210), buttonh);
			randomgame.setFont(topFontButton);
		}
		if(progressBar != null) progressBar.setBounds(scale(235), row5 - buttonh / 2, scale(170), buttonh);
		
		// PAUSED
		pausedDiff = scale(4);
		pausedFont = getNewFont(Font.SANS_SERIF, Font.BOLD, 56);
		
		// KEYBOARD
		kbBottom = scale(479);
		kbBHeight = scale(50);
		kbWLBottom = scale(473);
		kbBLBottom = scale(444);
		
		refreshDependKeyboardStateAndSize();
		
		// MIDDLE
		mdTop = topHeight;
		ScoreMessage.resized(ratio * 0.75);
		mdArc = scale(10);
	}
	
	/**
	 * Method called to actualise the dimension of the keyboard depending on the number of key
	 * {@code kbCount} the width of the display zone {@link Graphs#WIDTH}.
	 */
	private void refreshDependKeysAndSize() {
		// KEYBOARD
		kbWWidth = scale((Graphs.WIDTH - 1) / kbCount);
		kbBWidth = 2 * kbWWidth / 3;
		kbWidth = kbCount * kbWWidth;
		kbLeft = ((getWidth() - kbWidth) / 2);
		kbRight = kbLeft + kbWidth;
	}
	
	/**
	 * Method called to set and scale the dimensions of the keyboard and of the middle part
	 * according to the keyboard state {@code kbActive}.
	 */
	private void refreshDependKeyboardStateAndSize() {
		kbActive = Graphs.isKeyboard();
		if(kbActive) {
			mdBottom = scale(400);
			mdBottomU = 400;
			mdHeight = mdBottom - mdTop + 1;
			kbTop = mdBottom;
			kbWHeight = kbBottom - kbTop + 1;
			msgImageY = scale(405);
		} else {
			mdBottom = scale(479);
			mdBottomU = 479;
			mdHeight = mdBottom - mdTop + 1;
			kbTop = mdBottom;
			kbWHeight = kbBottom - kbTop + 1;
			msgImageY = scale(460);
		}
	}
	
	/**
	 * Method called to set the fields for the settings of the letters of the middle part given by
	 * the method {@code Graphs#getNoteLetters()} and for the settings of the letters for the
	 * keyboard given by the method {@code Graphs#getKeyboardLetters()}.
	 */
	private void refreshDependLetters() {
		mdLetters = Graphs.getNoteLetters();
		kbLetters = Graphs.getKeyboardLetters();
	}
	
	/**
	 * Method to set the keyboard letters font and the middle part font for the letters in function
	 * of the fields {@code kbLetters} and {@code mdLetters}.
	 */
	private void refreshDependLettersAndKeysAndSize() {
		switch(kbLetters) {
			case 1:
				kbFont = getNewFont(Font.SANS_SERIF, Font.PLAIN, 13 - kbCount / 10);
				break;
			case 2:
				kbFont = getNewFont(Font.SANS_SERIF, Font.PLAIN, 13 - kbCount / 5);
				break;
			case 3:
				kbFont = getNewFont(Font.SANS_SERIF, Font.PLAIN, 13 - kbCount / 5);
				break;
		}
		
		switch(mdLetters) {
			case 1:
				mdFont = getNewFont(Font.SANS_SERIF, Font.PLAIN, 13 - kbCount / 10);
				break;
			case 2:
				mdFont = getNewFont(Font.SANS_SERIF, Font.PLAIN, 13 - kbCount / 5);
				break;
			case 3:
				mdFont = getNewFont(Font.SANS_SERIF, Font.PLAIN, 13 - kbCount / 5);
				break;
		}
	}
	
	public void updateTexts() {
		repaint();
	}
	
	public void updateTheme() {
		URL url = Graphs.getBgMain();
		if(url == null) {
			bgMainOrig = null;
			bgTop.setObj(null);
			bgMain.setObj(null);
		} else {
			try {
				bgAllOrig = Util.TOOLKIT.getImage(url);
				
				final int height = bgAllOrig.getHeight(new ImageObserver() {
					public boolean imageUpdate(Image img, int infoflags, int x, int y, int w, int h) {
						if((infoflags & (ImageObserver.ABORT | ImageObserver.ERROR)) != 0) return false;
						if((infoflags | ImageObserver.HEIGHT) != 0) {
							cropImages(h);
							return false;
						}
						return true;
					}
				});
				
				if(height != -1) cropImages(height);
			} catch(Exception e) {
				bgMainOrig = null;
				bgTop.setObj(null);
				bgMain.setObj(null);
			}
		}
	}
	
	private void cropImages(int height) {
		final int y = (height * getMiddleTop() / 480);
		bgTopOrig = Util.TOOLKIT.createImage(new FilteredImageSource(bgAllOrig.getSource(), new CropImageFilter(0, 0, height * 4 / 3, y + 1)));
		bgMainOrig = Util.TOOLKIT.createImage(new FilteredImageSource(bgAllOrig.getSource(), new CropImageFilter(0, y, height * 4 / 3, height - y)));
		
		if(!Util.preloadIfToolkit(bgTopOrig, Util.IMAGE_OBSERVER)) {
			bgTopOrig.getWidth(Util.IMAGE_OBSERVER);
		}
		
		if(!Util.preloadIfToolkit(bgMainOrig, Util.IMAGE_OBSERVER)) {
			bgMainOrig.getWidth(Util.IMAGE_OBSERVER);
		}
		
		bgTop.setObj(bgMainOrig);
		bgMain.setObj(bgMainOrig);
		
		if(getWidth() != 0) {
			Util.resizeImage(bgTopOrig, getWidth(), bgTop, Graphs.SCALE_HINT);
			Util.resizeImage(bgMainOrig, getWidth(), bgMain, Graphs.SCALE_HINT);
		}
	}
	
	public void gameStarted() {
		Util.updateButtonText(pause, "Pause");
		pause.setEnabled(true);
		progressBar.setMaximum((int)(Game.getState().msLength / 100000));
	}
	
	public void gameEnded() {
		pause.setEnabled(false);
	}
	
	public void gamePaused() {
		Util.updateButtonText(pause, "Resume");
		repaint();
	}
	
	public void gameResumed() {
		Util.updateButtonText(pause, "Pause");
		repaint();
	}
	
	public void fullScreenStateChanged(boolean fullScreen) {
		Util.updateButtonText(fullscreen, (fullScreen ? "NormalScreen" : "FullScreen"));
		repaint();
	}
	
	public void setFullscreenChangeAllowed(boolean allowed) {
		fullscreen.setEnabled(allowed);
	}
	
	{
		updateTheme();
		refreshDependKeys();
		refreshDependSize();
		refreshDependKeysAndSize();
		refreshDependLetters();
		refreshDependLettersAndKeysAndSize();
	}
	
	//-------------------- BACKGROUND FUNCTIONS --------------------
	
	public void drawBackground(final Graphics g) {
		final Image image = bgMain.getObj();
		if(image != null) g.drawImage(image, 0, mdTop, this);
	}
	
	//-------------------- TOP FUNCTIONS --------------------
	
	/**
	 * Method for drawing the top of the screen. It display the components for the user name, the
	 * total and current score, the speed. If the game is started,the field {@code Game#isInGame()}
	 * is true, it also display the title , the author, the difficulty of the song being played, the
	 * current time.
	 * 
	 * @param g
	 *            the Graphic context to which the notes should be drawn
	 * @param s
	 *            the current state of the game
	 * 
	 */
	public void drawTop(Graphics g, State s) {
		final Image image = bgTop.getObj();
		if(image != null) g.drawImage(image, 0, 0, this);
		
		final FontRenderContext frc = ((Graphics2D)g).getFontRenderContext();
		
		g.setColor(Graphs.getColor("topText"));
		if(Util.getDebugLevel() > 385) g.drawLine(0, topHeight, getWidth(), topHeight);
		
		g.setFont(topFontLabel);
		
		drawStringRight(g, frc, topFontLabel, Util.getMsg("UserName") + ": ", col1, row1, col1w);
		drawStringRight(g, frc, topFontLabel, Util.getMsg("TotalScore") + ": ", col1, row2, col1w);
		drawStringRight(g, frc, topFontLabel, Util.getMsg("CurrentScore") + ": ", col1, row3, col1w);
		drawStringRight(g, frc, topFontLabel, Util.getMsg("Speed") + ": ", col1, row4, col1w);
		
		if(Game.isInGame()) {
			drawStringRight(g, frc, topFontLabel, Util.getMsg("Title") + ": ", col3, row1, col3w);
			drawStringRight(g, frc, topFontLabel, Util.getMsg("Author") + ": ", col3, row2, col3w);
			drawStringRight(g, frc, topFontLabel, Util.getMsg("Difficulty") + ": ", col3, row3, col3w);
			drawStringRight(g, frc, topFontLabel, Util.getMsg("Time") + ": ", col3, row4, col3w);
		} else {
			drawStringCenter(g, frc, topFontLabel, Util.getMsg("NoActiveGame"), col3, row3, col34w);
		}
		
		final String name = Util.getProp("name");
		if(name.length() == 0) {
			g.setFont(topFontValueItalic);
			g.drawString(Util.getMsg("Anonymous"), col2, row1);
			g.setFont(topFontValue);
		} else {
			g.setFont(topFontValue);
			g.drawString(name, col2, row1);
		}
		g.drawString(Integer.toString(s.totalScore), col2, row2);
		g.drawString(Integer.toString(s.score), col2, row3);
		g.drawString(Util.floatToSpeed(s.speed), col2, row4);
		
		if(Game.isInGame()) {
			g.drawString(s.songInfo.getTitle(), col4, row1);
			String cache = s.songInfo.getAuthor();
			g.drawString((cache == null ? Util.getMsg("Unknown") : cache), col4, row2);
			cache = DialogSongList.SongSelector.DIFFICULTIES.get(s.songInfo.getDifficulty());
			g.drawString((cache == null ? Util.getMsg("Unknown") : Util.getMsg(cache)), col4, row3);
			g.drawString(Util.microToStr(s.msPosition) + s.msLengthStr, col4, row4);
			progressBar.setValue((int)(s.msPosition / 100000));
		}
		
		if(Util.getPropBool("connClients")) {
			final List<Client> clients = Connection.getClients();
			Collections.sort(clients, new Comparator<Client>() {
				public int compare(Client o1, Client o2) {
					final int score1 = o1.getScore();
					final int score2 = o2.getScore();
					if(score1 < score2) {
						return -1;
					} else if(score1 == score2) {
						return o1.getActivity() - o2.getActivity();
					} else {
						return 1;
					}
				}
			});
			
			TreeSet<UUID> printeds = new TreeSet<UUID>();
			
			int i = 42;
			for(Client client : clients) {
				final UUID id = client.getId();
				if(!printeds.add(id)) {
					continue;
				}
				
				final int row = scale(i);
				
				final String player = client.getPlayerTrimmed();
				if(player.length() == 0) {
					g.setFont(topFontPlayersItalic);
					g.drawString(Util.getMsg("Anonymous") + " :", col5, row);
				} else {
					g.setFont(topFontPlayers);
					g.drawString(player + " :", col5, row);
				}
				
				g.setFont(topFontScores);
				final StringBuffer buff = new StringBuffer();
				switch(client.getActivity()) {
					case 1:
						buff.append(client.getScore());
						final String title = client.getTitle();
						if(title != null && !title.equals("")) {
							buff.append(" - (");
							buff.append(title);
							buff.append(')');
						}
						break;
					case 2:
						buff.append(client.getScore());
						buff.append(" - [");
						buff.append(Util.getMsg("InPause"));
						buff.append(']');
						break;
					case 3:
						buff.append(client.getScore());
						buff.append(" - [");
						buff.append(Util.getMsg((Util.getDebugLevel() >= 127 ? "InDebugPause" : "InPause")));
						buff.append(']');
						break;
					case 4:
						buff.append('[');
						buff.append(Util.getMsg("NotInGame"));
						buff.append(']');
						break;
				}
				g.drawString(buff.toString(), col6, row);
				
				if((i += 15) >= 100) {
					break;
				}
			}
		}
	}
	
	//-------------------- MIDDLE FUNCTIONS --------------------
	/**
	 * Method for displaying the falling notes in the middle part. It calculates the coordinates
	 * where the note and the corresponding letter should be displayed (according to the parameter
	 * {@code note}).
	 * 
	 * @param g
	 *            the current Graphics context.
	 * @param state
	 *            the current state of the game.
	 * @param note
	 *            the note to be drawn.
	 */
	public void drawNote(Graphics g, State state, Note note) {
		g.setFont(mdFont);
		Key key = note.key;
		
		final Color current = Util.getFadingColor((double)key.getVelocity() / Key.MAX_VELOCITY, Graphs.getColor("note"), Graphs.getColor("maxVelocity"));
		
		int x;
		int type = key.key % 7;
		final long lHeight = note.length * mdHeight / state.fallingTime;
		final int height = (int)lHeight;
		final int y = (int)(mdTop + (((state.time + state.fallingTime) - note.time) * mdHeight / state.fallingTime) - lHeight);
		if(type < 0) type += 7;
		
		FontRenderContext fontRenderContext = ((Graphics2D)g).getFontRenderContext();
		
		if(key.higher) {
			x = kbLeft + (key.key - kbMin + 1) * kbWWidth - kbBWidth / 2;
			g.setColor(current);
			g.fillRoundRect(x, y, kbBWidth - 1, height, mdArc, mdArc);
			g.setColor(Graphs.getColor("noteBorder"));
			g.drawRoundRect(x, y, kbBWidth - 1, height, mdArc, mdArc);
			g.setColor(Graphs.getColor("kbFontActive"));
			final String cache;
			final Rectangle2D bounds = mdFont.getStringBounds(cache = key.toStr(mdLetters), fontRenderContext);
			g.drawString(cache, x + (kbBWidth - (int)bounds.getWidth()) / 2, y + height - 1);
		} else {
			x = kbLeft + (key.key - kbMin) * kbWWidth;
			g.setColor(current);
			g.fillRoundRect(x, y, kbWWidth, height, mdArc, mdArc);
			g.setColor(Graphs.getColor("noteBorder"));
			g.drawRoundRect(x, y, kbWWidth, height, mdArc, mdArc);
			g.setColor(Graphs.getColor("kbFontActive"));
			final String cache;
			final Rectangle2D bounds = mdFont.getStringBounds(cache = key.toStr(mdLetters), fontRenderContext);
			g.drawString(cache, x + (kbWWidth - (int)bounds.getWidth()) / 2, y + height - 1);
		}
	}
	
	/**
	 * Method for displaying the score messages.
	 * 
	 * @param g
	 *            the current Graphics context.
	 * @param messages
	 *            a sorted set of score messages.
	 */
	
	public void drawScoreMessages(Graphics g, SortedSet<ScoreMessage> messages) {
		synchronized (messages) {
			final TreeMap<Integer, ScoreMessage> map = new TreeMap<Integer, ScoreMessage>();
			
			Iterator<ScoreMessage> iterator = messages.iterator();
			while(iterator.hasNext()) {
				ScoreMessage message = iterator.next();
				
				final int time = (int)(System.currentTimeMillis() - message.time);
				if(time > ScoreMessage.TIME_LIMIT) {
					iterator.remove();
				} else {
					//					//no fractional pt of fonts are supported by java...
					//					HashMap<TextAttribute, Object> map = new HashMap<TextAttribute, Object>();
					//					map.put(TextAttribute.FAMILY, ScoreMessage.FONT_NAME);
					//					map.put(TextAttribute.WEIGHT, ((ScoreMessage.FONT_STYLE & Font.BOLD) != 0 ? TextAttribute.WEIGHT_BOLD : TextAttribute.WEIGHT_REGULAR));
					//					map.put(TextAttribute.POSTURE, ((ScoreMessage.FONT_STYLE & Font.ITALIC) != 0 ? TextAttribute.POSTURE_OBLIQUE : TextAttribute.POSTURE_REGULAR));
					//					//map.put(TextAttribute.SIZE, (ScoreMessage.FONT_SIZE - (ScoreMessage.FONT_SIZE * time * ratio / ScoreMessage.TIME_LIMIT / 2)));
					//					map.put(TextAttribute.SIZE, ScoreMessage.FONT_SIZE);
					//					
					//					AffineTransform affiniteTransform = new AffineTransform();
					//					final double scale = 1 - (double)time / ScoreMessage.TIME_LIMIT / 2;
					//					affiniteTransform.setToScale(scale, scale);
					//					map.put(TextAttribute.TRANSFORM, new TransformAttribute(affiniteTransform));
					//					
					//					final Font font = new Font(map);
					
					final Font font = getNewFont(ScoreMessage.FONT_NAME, ScoreMessage.FONT_STYLE, ScoreMessage.FONT_SIZE - (ScoreMessage.FONT_SIZE * time / ScoreMessage.TIME_LIMIT / 2));
					
					g.setFont(font);
					g.setColor(message.color);
					g.drawString(message.getMessage(), message.getX(font), scale(message.getY()));
					
					if(Graphs.isScoreImages()) map.put(getScaledKeyX(message.key), message);
				}
			}
			
			if(Graphs.isScoreImages()) for(Map.Entry<Integer, ScoreMessage> entry : map.entrySet()) {
				final ScoreMessage message = entry.getValue();
				final Image image = message.getImage();
				g.drawImage(image, entry.getKey() - image.getWidth(this) / 2, msgImageY, this);
			}
		}
	}
	
	/**
	 * Method for drawing the representational keyboard.
	 * 
	 * @param g
	 *            the current Graphics context.
	 * @param activeKeys
	 *            a sorted map with the keys to be drawn.
	 */
	//-------------------- KEYBOARD FUNCTIONS --------------------
	public void drawKeyboard(Graphics g, SortedMap<Integer, SortedMap<Integer, SortedSet<Key>>> activeKeys) {
		if(!kbActive) return;
		
		// 0 1 23 4 5 6
		// c^d^ef^g^a^b
		
		g.setColor(Graphs.getColor("kbWhite"));
		g.fillRect(kbLeft, kbTop, kbWidth, kbWHeight);
		
		g.setColor(Graphs.getColor("kbBlack"));
		g.drawLine(kbLeft, kbTop, kbRight, kbTop);
		g.drawLine(kbLeft, kbBottom, kbRight, kbBottom);
		
		for(int x = kbLeft; x <= kbRight; x += kbWWidth) {
			g.drawLine(x, kbTop, x, kbBottom);
		}
		
		for(int i = 0, x = kbLeft + kbWWidth - kbBWidth / 2; i < (kbCount - 1); ++i, x += kbWWidth) {
			switch((i + kbStart) % 7) {
				case 2:
				case 6:
					break;
				default:
					g.fillRect(x, kbTop, kbBWidth, kbBHeight);
			}
		}
		
		if(kbLetters != 0) {
			g.setColor(Graphs.getColor("kbFontOnWhite"));
			g.setFont(kbFont);
			FontRenderContext fontRenderContext = ((Graphics2D)g).getFontRenderContext();
			String cache;
			for(int i = kbMin, x = kbLeft + kbWWidth / 2; i <= kbMax; ++i, x += kbWWidth) {
				g.drawString(cache = Key.toStr(kbLetters, i, false), x - (int)kbFont.getStringBounds(cache, fontRenderContext).getWidth() / 2, kbWLBottom);
			}
			
			g.setColor(Graphs.getColor("kbFontOnBlack"));
			for(int i = kbMin, x = kbLeft + kbWWidth; i < kbMax; ++i, x += kbWWidth) {
				switch((i + 126) % 7) {
					case 2:
					case 6:
						break;
					default:
						g.drawString(cache = Key.toStr(kbLetters, i, true), x - (int)kbFont.getStringBounds(cache, fontRenderContext).getWidth() / 2, kbBLBottom);
				}
			}
		}
		
		synchronized (activeKeys) {
			for(SortedMap<Integer, SortedSet<Key>> treeMap : activeKeys.values()) {
				synchronized (treeMap) {
					for(SortedSet<Key> treeSet : treeMap.values()) {
						synchronized (treeSet) {
							for(Key key : treeSet) {
								changeKeyStatus(key, g, true);
							}
						}
					}
				}
			}
		}
	}
	
	public void activateKey(Key key) {
		Graphics2D g = (Graphics2D)getGraphics();
		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
		changeKeyStatus(key, g, true);
	}
	
	public void deactivateKey(Key key) {
		Graphics2D g = (Graphics2D)getGraphics();
		g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
		changeKeyStatus(key, g, false);
	}
	
	/**
	 * Method for displaying to the screen if a key has been pressed / released and with what
	 * pressure (the highest, the more intense colour for the key).
	 * 
	 * @param key
	 *            the key to be drawn.
	 * @param g
	 *            the current Graphics context.
	 * @param isActive
	 *            boolean indicating if the note has been pressed or released.
	 */
	private void changeKeyStatus(Key key, Graphics g, boolean isActive) {
		if(key.key < kbMin || key.key > kbMax || !kbActive) return;
		int type = key.key % 7;
		if(type < 0) type += 7;
		
		if(key.higher) {
			switch(type) {
				case 2:
				case 6:
					break;
				default:
					final int x = kbLeft + (key.key - kbMin + 1) * kbWWidth;
					if(isActive) {
						g.setColor(Util.getFadingColor((double)key.getVelocity() / Key.MAX_VELOCITY, Graphs.getColor("note"), Graphs.getColor("maxVelocity")));
					} else {
						g.setColor(Graphs.getColor("kbBlack"));
					}
					g.fillRect(x - kbBWidth / 2 + 1, kbTop + 1, kbBWidth - 2, kbBHeight - 2);
					if(kbLetters != 0) {
						FontRenderContext fontRenderContext = ((Graphics2D)g).getFontRenderContext();
						final String cache;
						g.setFont(kbFont);
						g.setColor((isActive ? Graphs.getColor("kbFontActive") : Graphs.getColor("kbFontOnBlack")));
						g.drawString(cache = key.toStr(kbLetters), x - (int)kbFont.getStringBounds(cache, fontRenderContext).getWidth() / 2, kbBLBottom);
					}
			}
		} else {
			final int x = kbLeft + (key.key - kbMin) * kbWWidth;
			if(isActive) {
				g.setColor(Util.getFadingColor((double)key.getVelocity() / Key.MAX_VELOCITY, Graphs.getColor("note"), Graphs.getColor("maxVelocity")));
			} else {
				g.setColor(Graphs.getColor("kbWhite"));
			}
			
			if(key.key == kbMin) switch(type) {
				case 2:
				case 6:
					type = 7;
					break;
				case 1:
				case 4:
				case 5:
					type = 0;
					break;
			}
			
			if(key.key == kbMax) switch(type) {
				case 0:
				case 3:
					type = 7;
					break;
				case 1:
				case 4:
				case 5:
					type = 2;
					break;
			}
			
			switch(type) {
				case 0:
				case 3:
					g.fillRect(x + 1, kbTop + 1, kbWWidth - kbBWidth / 2 - 1, kbWHeight - 2);
					g.fillRect(x + kbWWidth - kbBWidth / 2, kbTop + kbBHeight, kbBWidth / 2, kbWHeight - kbBHeight - 1);
					break;
				case 2:
				case 6:
					g.fillRect(x + (kbBWidth + 1) / 2, kbTop + 1, kbWWidth - (kbBWidth + 1) / 2, kbWHeight - 2);
					g.fillRect(x + 1, kbTop + kbBHeight, kbBWidth / 2, kbWHeight - kbBHeight - 1);
					break;
				case 7:
					g.fillRect(x + 1, kbTop + 1, kbWWidth - 1, kbWHeight - 2);
					break;
				default:
					g.fillRect(x + kbBWidth / 2 + (kbBWidth % 2), kbTop + 1, kbWWidth - kbBWidth, kbBHeight - 1);
					g.fillRect(x + 1, kbTop + kbBHeight, kbWWidth - 1, kbWHeight - kbBHeight - 1);
					break;
			}
			
			if(kbLetters != 0) {
				FontRenderContext fontRenderContext = ((Graphics2D)g).getFontRenderContext();
				final String cache;
				g.setFont(kbFont);
				g.setColor((isActive ? Graphs.getColor("kbFontActive") : Graphs.getColor("kbFontOnWhite")));
				g.drawString(cache = key.toStr(kbLetters), x + (kbWWidth - (int)kbFont.getStringBounds(cache, fontRenderContext).getWidth()) / 2, kbWLBottom);
			}
		}
	}
	
	//-------------------- PROVIDING FUNCTIONS --------------------
	
	public final int getKeyX(Key key) {
		if(key.higher) {
			return kbLeftU + (key.key - kbMin + 1) * kbWWidthU;
		} else {
			return kbLeftU + (key.key - kbMin) * kbWWidthU + kbWWidthU / 2;
		}
	}
	
	public final int getScaledKeyX(Key key) {
		if(key.higher) {
			return kbLeft + (key.key - kbMin + 1) * kbWWidth;
		} else {
			return kbLeft + (key.key - kbMin) * kbWWidth + kbWWidth / 2;
		}
	}
	
	public final int getMiddleTop() {
		return 100;
	}
	
	public final int getMiddleBottom() {
		return mdBottomU;
	}
	
	//-------------------- DEBUGGING FUNCTIONS --------------------
	
	/**
	 * Creates a string containing the most important information about the game. This method is
	 * used only for debugging and testing purposes.
	 * 
	 * @return the created string.
	 */
	static String getString() { //$SUP-MCP$
		return "RendererSwing()";
	} // end method
	
	/**
	 * This method serves security purposes. Provides an integrity string that will be checked by
	 * the {@link Connection#integrityCheck()} method; thus the application can only be altered if
	 * the source is known. Every class in the {@link keyboardhero} package has an integrity string.
	 * 
	 * @return the string of this class used for integrity checking.
	 */
	static String getIntegrityString() {
		return "Oa+4y!-Łs÷i3SD@n_12{";
	} // end method
	
	/**
	 * The tester object of this class. It provides a debugging menu and unit tests for this class.
	 * Its only purpose is debugging or testing.
	 */
	final static Tester TESTER = new Tester("RendererSwing", new String[] {"getString()"} /* end initialization */) { //$SUP-PPA$
		void menu(int choice) throws Exception {
			switch(choice) {
				case 5:
					System.out.println(getString());
					break;
				default:
					baseMenu(choice);
					break;
			} // end switch
		} // end method
		
		void runUnitTests() throws Exception {
			higherTestStart("RendererSwing");
			
			testEq("getIntegrityString()", "Oa+4y!-Łs÷i3SD@n_12{", RendererSwing.getIntegrityString());
			
			higherTestEnd();
		} // end method
	}; // end class
	
	/**
	 * Starts the class's developing menu. If this build is a developer's one it starts the
	 * application in a normal way with the exception that it starts the debugging tool for this
	 * class as well; otherwise exits with an error message.
	 * 
	 * @param args
	 *            the arguments given to the program.
	 * @see KeyboardHero#startApp()
	 */
	public static void main(String[] args) {
		Tester.mainer(null, TESTER);
	} // end method
}
